package com.ale.oxetool.service;

import com.ale.oxetool.model.Phone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.*;
import java.util.concurrent.BlockingQueue;

/**
 * Created by guoshunw on 2016/5/19.
 */
public abstract class OXEConnection implements RemoteCommandService {
    private String host;
    private int port;
    private String username;
    private String password;

    private long readTimeout = 1000;

    private static Logger log = LoggerFactory.getLogger(OXEConnection.class);
    String prompt = ">";
    String serverTimeoutDisabledCmd = "timout -";
    private String charset = "UTF-8";

    BufferedInputStream bis;
    protected PrintWriter out;

    static {
//        ((ch.qos.logback.classic.Logger) log).setLevel(Level.DEBUG);
    }

    private BlockingQueue<RemoteCommandService> pool;


    void setReadTimeout(long readTimeout) {
        this.readTimeout = readTimeout;
    }

    public void setPool(BlockingQueue<RemoteCommandService> pool) {
        this.pool = pool;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public static Logger getLog() {
        return log;
    }

    public String getCharset() {
        return charset;
    }

    public void setCharset(String charset) {
        this.charset = charset;
    }


    public OXEConnection(String host, String username, String password) {
        this.host = host;
        this.username = username;
        this.password = password;
    }

    public OXEConnection(OXEServerSettings oxeServerSettings, boolean autoLogin) {

        this(oxeServerSettings.getHost(), oxeServerSettings.getUsername(), oxeServerSettings.getPassword());
        this.port = oxeServerSettings.getPort();
    }

    public OXEConnection(OXEServerSettings oxeServerSettings) {
        this(oxeServerSettings, true);
    }

    public abstract boolean isSSHConnection();

    public abstract boolean login(String username, String password) throws IOException;

    public abstract boolean connect(boolean autoLogin) throws IOException;

    public abstract void disconnect();

    private BufferedInputStream getInputStream() {
        return bis;
    }

    private PrintWriter getWriter() {
        return out;
    }


    public abstract boolean isConnected();

    @Override
    public boolean connect() throws IOException {
        if (isConnected()) return true;
        return this.connect(true);
    }

    String readUntilForLines(String... signs) throws IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader br = new BufferedReader(new InputStreamReader(bis));
        String line;
        while (null != (line = br.readLine())) {
            line = line.trim();
            for (String sign : signs) {
                if (line.contains(sign)) break;
            }
            sb.append(line);
            sb.append("\n");
        }
        return sb.toString();
    }

    String readUntil(String... patterns) {
        // nothing need to read
        if (null == patterns) {
            return "Nothing need to read, so I am back.";
        }

        try {
            StringBuilder sb = new StringBuilder();
            byte[] buf;
            int bufSize;
            boolean match = false;
            long beginReadStamp = System.currentTimeMillis();

            while (true) {
                bufSize = bis.available();
                if (bufSize <= 0) {
                    // if not matched, wait until data available or timeout
                    if (System.currentTimeMillis() - beginReadStamp > readTimeout) {
                        log.info("Read data time out...");
                        break;
                    }
                    if (!match) {
                        continue;
                    } else {
                        // match and no data available, end read
                        if (0 == bis.available()) {
                            break;
                        }
                    }
                }
                buf = new byte[bufSize];
                int size = bis.read(buf);
                if (size < bufSize) {
                    log.error("Read buffer {} less than expected {}", size, bufSize);
                }
                sb.append(new String(buf));
                // read the rest of output text
                String result = sb.toString().trim();

                for (String pattern : patterns) {
                    if (result.endsWith(pattern)) {
                        match = true;
                    }
                }
            }

            if (!match ) {
                log.warn("Read text doesn't match any of {}, which is \n{}", patterns, sb.toString());
            }
            return sb.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    public String sendCommand(String command) throws IOException {
        return sendCommand(command, prompt);
    }

    public String sendCommand(String command, String sign) {
        writeText(command);
        if (null == sign) {
            return null;
        }

        String result = readUntil(sign).trim();
        if (!result.startsWith(command.trim()) && !isSSHConnection()) {
            log.info("Command \"{}\" to host {} result is not expected, which is : {}\n", command, host, result);
        }
        return result;
    }


    void writeText(String text) {
        getWriter().print(text + "\n");
        getWriter().flush();
    }


    public List<Phone> getPhoneInfoFromOXE() throws IOException {
        return getPhoneInfoFromOXE(false);
    }

    public List<Phone> getPhoneInfoFromOXE(boolean autoDisconnect) throws IOException {

        if (!isConnected()) {
            connect(true);
        }
        String cmd = "tool ippstat list 19 1";
        String resultText = sendCommand(cmd);
        if (null == resultText || !resultText.startsWith(cmd) && !this.isSSHConnection()) {
            log.debug("cmd {} on OXEServer {} out is not expected, which is: {}", cmd, host, resultText);
            return Collections.emptyList();
        }


        List<Phone> phones = parseStringToPhones(resultText);
        log.debug("back to prompt:\n {}\n", resultText);
        log.debug("phones on {}: \n{}\n", host, phones);
        if (autoDisconnect) {
            disconnect();
        }
        return phones;
    }

    private List<Phone> parseStringToPhones(String outText) throws IOException {
        List<Phone> phones = new ArrayList<>();
        BufferedReader bf = new BufferedReader(new StringReader(outText));
        String line;
        Map<Integer, String> columnHeader = new HashMap<>();

        Phone phone;
        String headLineSign = "Mac Address";
        log.debug("Parsing OXE server {} output for phone list:\n{}\n", host, outText);

        while (null != (line = bf.readLine())) {

            if (!line.startsWith("|")) continue;
            String[] elements = line.split("\\s*\\|\\s*");
            if (line.contains(headLineSign)) { // this is header line
                log.debug("header line on {}=>{}", host, line);
                for (int i = 0; i < elements.length; ++i) {
                    columnHeader.put(i, elements[i].toLowerCase());
                }
                continue;
            }
            if (columnHeader.isEmpty()) {
                log.error("OXE Server {} doesn't have any phone on it.\n The OXE server output text: \n{}\n", host, outText);
            }

            log.debug("Info line=>{}", line);
            // phone information
            int index = 0;
            String columnName;
            Map<String, String> phoneValueMap = new HashMap<>();
            for (String element : elements) {
                columnName = columnHeader.get(index++);
                phoneValueMap.put(columnName, element);
            }
            String ipAddr = phoneValueMap.get("Ipv4 Address".toLowerCase());
            if (null == ipAddr) {
                ipAddr = phoneValueMap.get("Ip Address".toLowerCase());
            }
            phone = new Phone(
                    phoneValueMap.get("Name".toLowerCase()),
                    phoneValueMap.get(headLineSign.toLowerCase()),
                    ipAddr,
                    phoneValueMap.get("Type".toLowerCase()),
                    phoneValueMap.get("QMCDU".toLowerCase())
            );
            phone.setOxeServerAddr(this.host);
            phones.add(phone);
        }
        bf.close();
        return phones;
    }

    @Override
    public void release() throws InterruptedException {
        if (null == pool) return;
        log.debug("Release connection({}) back into pool {}", this, pool);
        pool.put(this);
    }
}
